/*
 *    Copyright 2009-2022 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       https://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.apache.ibatis.binding;

import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import org.apache.ibatis.annotations.Flush;
import org.apache.ibatis.annotations.MapKey;
import org.apache.ibatis.cursor.Cursor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.mapping.StatementType;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.ParamNameResolver;
import org.apache.ibatis.reflection.TypeParameterResolver;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.session.SqlSession;

/**
 * @author Clinton Begin
 * @author Eduardo Macarron
 * @author Lasse Voss
 * @author Kazuki Shimizu
 */
public class MapperMethod {

  private final SqlCommand command;
  private final MethodSignature method;

  /**
   * 构造 SqlCommand 和 MethodSignature 内部对象
   *
   * @param mapperInterface ？
   * @param method          被调用的方法
   * @param config          全局配置对象
   */
  public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
    this.command = new SqlCommand(config, mapperInterface, method);
    this.method = new MethodSignature(config, mapperInterface, method);
  }

  /**
   * 方法执行
   *
   * @param sqlSession
   * @param args
   * @return
   */
  public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;

    // 1. 判断命令类型（执行模式）
    switch (command.getType()) {
      case INSERT: {
        // 2. 取 参数名 -> 参数值 的 ParamMap
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      case UPDATE: {
        // ...
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.update(command.getName(), param));
        break;
      }
      case DELETE: {
        // ...
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.delete(command.getName(), param));
        break;
      }
      case SELECT:
        if (method.returnsVoid() && method.hasResultHandler()) {
          // 3. 方法返回结果为 void 且粗壮乃结果集处理器传参
          executeWithResultHandler(sqlSession, args);
          result = null;
        } else if (method.returnsMany()) {
          // 4. 返回结果为集合
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
          // 5. 返回结果为 Map
          result = executeForMap(sqlSession, args);
        } else if (method.returnsCursor()) {
          // 6. 返回结果为 游标
          result = executeForCursor(sqlSession, args);
        } else {
          // 7. 非以上情况则查询一个结果
          Object param = method.convertArgsToSqlCommandParam(args);
          result = sqlSession.selectOne(command.getName(), param);
          // 8. 若方法返回结果为 Optional 且 （结果集为空或方法返回类型补登与结果集返回类型），则返回封装结果集的Optional对象
          if (method.returnsOptional()
              && (result == null || !method.getReturnType().equals(result.getClass()))) {
            result = Optional.ofNullable(result);
          }
        }
        break;
      case FLUSH:
        // 9. 批量刷新语句对象
        result = sqlSession.flushStatements();
        break;
      default:
        throw new BindingException("Unknown execution method for: " + command.getName());
    }

    // 10. 不可访问的私有类型抛出异常
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
      throw new BindingException("Mapper method '" + command.getName()
          + "' attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
  }

  /**
   * 结果集数量处理
   * 返回结果集数量类型默认为 int，若方法的返回值类型非 int ，则通过此处做一个类型转换
   *
   * @param rowCount
   * @return
   */
  private Object rowCountResult(int rowCount) {
    final Object result;
    if (method.returnsVoid()) {
      result = null;
    } else if (Integer.class.equals(method.getReturnType()) || Integer.TYPE.equals(method.getReturnType())) {
      result = rowCount;
    } else if (Long.class.equals(method.getReturnType()) || Long.TYPE.equals(method.getReturnType())) {
      result = (long) rowCount;
    } else if (Boolean.class.equals(method.getReturnType()) || Boolean.TYPE.equals(method.getReturnType())) {
      result = rowCount > 0;
    } else {
      throw new BindingException("Mapper method '" + command.getName() + "' has an unsupported return type: " + method.getReturnType());
    }
    return result;
  }

  private void executeWithResultHandler(SqlSession sqlSession, Object[] args) {
    // 1. 通过语句对象ID获取全局配置对象
    MappedStatement ms = sqlSession.getConfiguration().getMappedStatement(command.getName());
    if (!StatementType.CALLABLE.equals(ms.getStatementType())
        && void.class.equals(ms.getResultMaps().get(0).getType())) {
      throw new BindingException("method " + command.getName()
          + " needs either a @ResultMap annotation, a @ResultType annotation,"
          + " or a resultType attribute in XML so a ResultHandler can be used as a parameter.");
    }
    // 2. 将参数转换成 ParamMap
    Object param = method.convertArgsToSqlCommandParam(args);
    if (method.hasRowBounds()) {
      // 3. 取分页对象
      RowBounds rowBounds = method.extractRowBounds(args);
      // 4. 执行查询
      sqlSession.select(command.getName(), param, rowBounds, method.extractResultHandler(args));
    } else {
      // ...
      sqlSession.select(command.getName(), param, method.extractResultHandler(args));
    }
  }

  private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
    List<E> result;
    // 1. 取 参数名 -> 参数值
    Object param = method.convertArgsToSqlCommandParam(args);

    // 2. 分页或不分页查询
    if (method.hasRowBounds()) {
      RowBounds rowBounds = method.extractRowBounds(args);
      result = sqlSession.selectList(command.getName(), param, rowBounds);
    } else {
      result = sqlSession.selectList(command.getName(), param);
    }
    // issue #510 Collections & arrays support
    // 集合或数组方面的支持
    // 3. 方法的返回类型与实际返回类型不服（实际返回类型默认为List）
    if (!method.getReturnType().isAssignableFrom(result.getClass())) {
      if (method.getReturnType().isArray()) {
        // 4. 若方法返回类型为数组，则将结果集转换为数组
        return convertToArray(result);
      } else {
        // 5. 若方法返回类型为其它集合则做一个对应返回结果类型的封装
        return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
      }
    }
    return result;
  }

  private <T> Cursor<T> executeForCursor(SqlSession sqlSession, Object[] args) {
    Cursor<T> result;
    // ...
    Object param = method.convertArgsToSqlCommandParam(args);
    if (method.hasRowBounds()) {
      RowBounds rowBounds = method.extractRowBounds(args);
      result = sqlSession.selectCursor(command.getName(), param, rowBounds);
    } else {
      result = sqlSession.selectCursor(command.getName(), param);
    }
    return result;
  }

  /**
   * List -> 方法返回类型的容器
   */
  private <E> Object convertToDeclaredCollection(Configuration config, List<E> list) {
    /**
     * {@link org.apache.ibatis.reflection.factory.DefaultObjectFactory#resolveInterface}
     */
    // 1. 创建方法返回类型的实例
    Object collection = config.getObjectFactory().create(method.getReturnType());
    // 2. 包装成 MetaObject 方法后添加数据
    MetaObject metaObject = config.newMetaObject(collection);
    metaObject.addAll(list);
    return collection;
  }

  /**
   * List -> Array
   */
  @SuppressWarnings("unchecked")
  private <E> Object convertToArray(List<E> list) {
    // 1. 取对象类型
    Class<?> arrayComponentType = method.getReturnType().getComponentType();
    // 2. 实例化一个对象数组，并指定大小
    Object array = Array.newInstance(arrayComponentType, list.size());
    // 3. 如果是基本数据类型则向数组中逐个添加元素，否则直接调用数组方法
    if (arrayComponentType.isPrimitive()) {
      for (int i = 0; i < list.size(); i++) {
        Array.set(array, i, list.get(i));
      }
      return array;
    } else {
      return list.toArray((E[]) array);
    }
  }

  private <K, V> Map<K, V> executeForMap(SqlSession sqlSession, Object[] args) {
    Map<K, V> result;
    // ...
    Object param = method.convertArgsToSqlCommandParam(args);
    if (method.hasRowBounds()) {
      RowBounds rowBounds = method.extractRowBounds(args);
      result = sqlSession.selectMap(command.getName(), param, method.getMapKey(), rowBounds);
    } else {
      result = sqlSession.selectMap(command.getName(), param, method.getMapKey());
    }
    return result;
  }

  /**
   * 参数字典，继承自 HashMap
   * 仅重写了一个 get 方法，添加了当Map中不包含指定 key 时，
   * 抛出“Parameter '" + key + "' not found. Available parameters are " + keySet()” 异常
   *
   * @param <V>
   */
  public static class ParamMap<V> extends HashMap<String, V> {

    private static final long serialVersionUID = -2212268410512043556L;

    @Override
    public V get(Object key) {
      if (!super.containsKey(key)) {
        throw new BindingException("Parameter '" + key + "' not found. Available parameters are " + keySet());
      }
      return super.get(key);
    }

  }

  /**
   * SqlCommand 为 MapperMethod 类的 execute() 方法提供了 查询类型 和 方法id 两个信息，从而使用 Sqlseesion 执行不同的方法
   */
  public static class SqlCommand {

    /**
     * MappedStatement对象中的 id
     */
    private final String name;

    /**
     * 枚举对象，包括：UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH
     */
    private final SqlCommandType type;

    public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
      // 1. 取被调用方法名
      final String methodName = method.getName();
      // 2. 取被调用方法所属对象类型
      final Class<?> declaringClass = method.getDeclaringClass();

      // 3. 取映射语句
      MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
          configuration);
      if (ms == null) {
        // 4. 若映射语句为空，则判断该方法是否被 @Flush 注解标注
        // (如果使用 @Flush ，则可以通过在Mapper接口中定义的方法调用sqlsession# flushStatements()。(MyBatis 3.3或以上))
        if (method.getAnnotation(Flush.class) != null) {
          name = null;
          type = SqlCommandType.FLUSH;
        } else {
          throw new BindingException("Invalid bound statement (not found): "
              + mapperInterface.getName() + "." + methodName);
        }
      } else {
        // 5. 取映射语句中的 id 作为 name， 并将 SqlCommandType 赋值给 type
        name = ms.getId();
        type = ms.getSqlCommandType();
        // 6. 如果 SqlCommandType 为 UNKNOWN 则抛出“未知执行方法”异常；
        if (type == SqlCommandType.UNKNOWN) {
          throw new BindingException("Unknown execution method for: " + name);
        }
      }
    }

    public String getName() {
      return name;
    }

    public SqlCommandType getType() {
      return type;
    }

    /**
     * 通过当前类型及其父类型和方法名构造语句ID，然后全局配置类中查找映射语句对象，若未找到则返回 null
     *
     * @param mapperInterface
     * @param methodName      方法名称
     * @param declaringClass  调用方法对象类型
     * @param configuration   全局配置对象
     * @return  映射语句
     */
    private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
        Class<?> declaringClass, Configuration configuration) {
      // 1. 构造语句对象ID （语句对象ID 一般时类的全限定域名+.方法名)
      String statementId = mapperInterface.getName() + "." + methodName;
      // 2. 如果全局配置对象中存在这个语句对象则直接返回
      if (configuration.hasStatement(statementId)) {
        return configuration.getMappedStatement(statementId);
      } else if (mapperInterface.equals(declaringClass)) {
        // 3. 全局配置对象中不存在且当前 mapper 对象类型为接口，则返回null （这个 null 可能导致后续抛出“不存在语句对象”的异常）
        return null;
      }

      // 4. 全局配置对象中不存在且当前 mapper 对象类型为不为接口，则遍历当前类型实现的接口
      for (Class<?> superInterface : mapperInterface.getInterfaces()) {
        // 4.1. 当前类型实现的某一接口是被调用方法所属类型的子类（因为上面的代码没有从调用方法的当前对象类型中找到，所以现在取父类找，
        // 因为此时该方法可能是当前类型父类或接口中定义好的方法）
        // 判断 superInterface 是否是 declaringClass 的继承（实现）类，或相同类
        if (declaringClass.isAssignableFrom(superInterface)) {
          // 4.2。 递归调用，在当前类型的父类中寻找该方法的 语句对象
          MappedStatement ms = resolveMappedStatement(superInterface, methodName,
              declaringClass, configuration);
          if (ms != null) {
            return ms;
          }
        }
      }
      return null;
    }
  }

  /**
   * MethodSignature 包含了被拦截方法的一些信息，如目标方法的返回类型，目标方法的参数列表信息等
   */
  public static class MethodSignature {

    private final boolean returnsMany;
    private final boolean returnsMap;
    private final boolean returnsVoid;
    private final boolean returnsCursor;
    private final boolean returnsOptional;
    private final Class<?> returnType;
    private final String mapKey;
    private final Integer resultHandlerIndex;
    private final Integer rowBoundsIndex;
    private final ParamNameResolver paramNameResolver;

    public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {
      // 1. 通过反射解析方法返回类型
      Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
      if (resolvedReturnType instanceof Class<?>) {
        this.returnType = (Class<?>) resolvedReturnType;
      } else if (resolvedReturnType instanceof ParameterizedType) {
        this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType();
      } else {
        this.returnType = method.getReturnType();
      }
      // 2. 检测返回值类型是否是 void、集合或数组、Cursor、Map 等
      this.returnsVoid = void.class.equals(this.returnType);
      this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray();
      this.returnsCursor = Cursor.class.equals(this.returnType);
      this.returnsOptional = Optional.class.equals(this.returnType);

      // 3. 解析 @MapKey 注解，获取注解内容
      this.mapKey = getMapKey(method);
      this.returnsMap = this.mapKey != null;

      /*
       * 获取 RowBounds 参数在参数列表中的位置，如果参数列表中
       * 包含多个 RowBounds 参数，此方法会抛出异常
       */
      this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);

      // 4. 获取 ResultHandler 参数在参数列表中的位置
      this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);

      // 5. 解析参数列表
      this.paramNameResolver = new ParamNameResolver(configuration, method);
    }

    /**
     * 返回一个
     * 参数名 -> 参数值
     * param1参数名 -> 参数值
     * 的集合
     *
     * @param args
     * @return
     */
    public Object convertArgsToSqlCommandParam(Object[] args) {
      return paramNameResolver.getNamedParams(args);
    }

    public boolean hasRowBounds() {
      return rowBoundsIndex != null;
    }

    public RowBounds extractRowBounds(Object[] args) {
      return hasRowBounds() ? (RowBounds) args[rowBoundsIndex] : null;
    }

    public boolean hasResultHandler() {
      return resultHandlerIndex != null;
    }

    public ResultHandler extractResultHandler(Object[] args) {
      return hasResultHandler() ? (ResultHandler) args[resultHandlerIndex] : null;
    }

    public Class<?> getReturnType() {
      return returnType;
    }

    public boolean returnsMany() {
      return returnsMany;
    }

    public boolean returnsMap() {
      return returnsMap;
    }

    public boolean returnsVoid() {
      return returnsVoid;
    }

    public boolean returnsCursor() {
      return returnsCursor;
    }

    /**
     * return whether return type is {@code java.util.Optional}.
     *
     * @return return {@code true}, if return type is {@code java.util.Optional}
     * @since 3.5.0
     */
    public boolean returnsOptional() {
      return returnsOptional;
    }

    private Integer getUniqueParamIndex(Method method, Class<?> paramType) {
      Integer index = null;
      // 1. 取方法的参数列表类型
      final Class<?>[] argTypes = method.getParameterTypes();
      for (int i = 0; i < argTypes.length; i++) {
        // 2. 参数列表中某参数类型是paramType或其子类
        if (paramType.isAssignableFrom(argTypes[i])) {
          if (index == null) {
            // 3. 设置index，即找到该类型在参数列表中的未知
            index = i;
          } else {
            // 4. 不可以包含两个相同 paramType 类型的参数
            throw new BindingException(method.getName() + " cannot have multiple " + paramType.getSimpleName() + " parameters");
          }
        }
      }
      return index;
    }

    public String getMapKey() {
      return mapKey;
    }

    private String getMapKey(Method method) {
      String mapKey = null;
      // 当前方法的返回类型为 Map
      if (Map.class.isAssignableFrom(method.getReturnType())) {
        final MapKey mapKeyAnnotation = method.getAnnotation(MapKey.class);
        if (mapKeyAnnotation != null) {
          // 取 MapKey 注解中定义的值
          mapKey = mapKeyAnnotation.value();
        }
      }
      return mapKey;
    }
  }

}
